package org.iodocs.mojo;

import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.cxf.jaxrs.model.wadl.Description;
import org.apache.cxf.jaxrs.model.wadl.Descriptions;
import org.iodocs.annotations.*;
import org.iodocs.annotations.IoDocsParameter.Location;
import org.iodocs.annotations.IoDocsParameter.Type;
import org.springframework.util.StringUtils;

import javax.ws.rs.*;
import java.io.File;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Scanner;

/**
 * User: Anjana D. Kulasinghe
 * Date: 9/26/13
 */

public class IoDocGenerator {

    private static final List<Class<?>> validRequestBodyClasses = Lists.newArrayList(String.class,
            byte[].class,
            java.io.InputStream.class,
            java.io.Reader.class,
            File.class,
            javax.activation.DataSource.class,
            javax.xml.transform.Source.class,
            javax.xml.bind.JAXBElement.class,
            javax.ws.rs.core.MultivaluedMap.class,
            javax.ws.rs.core.StreamingOutput.class);


    /**
     * Generate Mashery I/O Docs based on annotated JAX-RS classes
     * <p/>
     * Features
     * - Recognises HeaderParam, QueryParam, PathParam, and the
     * supported request body serialisation classes (String, InputStream etc..)
     * - Supports Integer, Boolean and String parameter types directly
     * - All other parameter types will use I/O Docs type 'string'
     * - Boolean supported as "true"/"false" enumeration
     * - Supports default parameter values for the supported parameter types
     * - IoDocsDefaultBoolean
     * - IoDocsDefaultString
     * - IoDocsDefaultInteger
     * - Supports removal of selected methods or parameters from I/O docs using
     * - IoDocsIgnore
     * - Supports required properties using IoDocsRequired
     * - Supports Enumeration values and descriptions using IoDocsEnum(Description)
     * - Supports parameter extensions to enhance regularly used
     * parameters
     *
     * @return The I/O Docs JSON
     * @parameter name                The name of the API (not shown to the public)
     * @parameter title               The title of the API
     * @parameter description         A more detailed description of the API
     * @parameter version             The current version of the API
     * @parameter basePath            The base path from which all paths are relative
     * @parameter endpoints           The classes that contain JAX-RS annotated RESTful methods
     * @parameter outputFile          The Path of the file create for upload
     */
    public String generateIoDoc(
            String name,
            String title,
            String description,
            String version,
            String basePath,
            Class<?>[] endpoints,
            String outputFile) {
        List<IoDocsParameter> extensionParameters = Lists.newArrayList();
        LinkedHashMap<String, Object> json = Maps.newLinkedHashMap();
        LinkedHashMap<String, Object> auth = Maps.newLinkedHashMap();
        LinkedHashMap<String, Object> key = Maps.newLinkedHashMap();
        LinkedHashMap<String, Object> methodList;
        LinkedHashMap<String, Object> methods;
        LinkedHashMap<String, Object> methodGroups = Maps.newLinkedHashMap();
        json.put("name", name);
        json.put("title", title);
        json.put("description", description);
        json.put("version", version);
        json.put("basePath", basePath);
        json.put("protocol", "rest");
        json.put("auth", auth);
        auth.put("key", key);
        key.put("location", "query");
        key.put("param", "api_key");
        json.put("resources", methodGroups);

        for (Class<?> endpoint : endpoints) {
            methods = Maps.newLinkedHashMap();
            methodList = Maps.newLinkedHashMap();
            for (IoDocsMethod method : getMethodsFromEndpoint(endpoint)) {
                methods.put(method.getName(), method.getData(extensionParameters));
            }
            methodList.put("methods", methods);
            methodGroups.put(endpoint.getSimpleName(), methodList);
        }

        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String ioDoc = gson.toJson(json);

        // Generate IO Doc File To Upload
        try {
            PrintWriter writer = new PrintWriter(outputFile);
            writer.println(ioDoc);
            writer.close();
        } catch (Exception ex) {
            System.out.println("Generating IO Doc to upload failed. " + ex.getMessage());

        }
        return gson.toJson(json);
    }

    private <T> List<IoDocsMethod> getMethodsFromEndpoint(
            Class<T> jaxRsClass) {
        List<IoDocsMethod> methods = Lists.newArrayList();
        String endpointPath = "";
        if (jaxRsClass.getAnnotation(Path.class) != null)
            endpointPath = jaxRsClass.getAnnotation(Path.class).value();

        for (Method method : jaxRsClass.getMethods()) {
            String name = jaxRsClass.getSimpleName() + "_" + method.getName();
            String httpMethod = null;
            String path = null;
            String description = null;
            List<IoDocsParameter> parameters = Lists.newArrayList();

            if (method.isAnnotationPresent(IoDocsIgnore.class)) continue;

            for (Annotation annotation : method.getAnnotations()) {
                if (annotation.annotationType().equals(IoDocsName.class)) {
                    name = ((IoDocsName) annotation).value();
                } else if (annotation.annotationType().isAnnotationPresent(HttpMethod.class)) {
                    String restMethodPath = endpointPath;
                    if (method.getAnnotation(Path.class) != null)
                        restMethodPath = endpointPath + method.getAnnotation(Path.class).value();

                    Descriptions descriptions = method.getAnnotation(Descriptions.class);

                    httpMethod = annotation.annotationType().getAnnotation(HttpMethod.class).value();
                    path = restMethodPath;

                    if (descriptions != null) {
                        String fullDescription = StringUtils.collectionToCommaDelimitedString(
                                Lists.transform(Lists.newArrayList(descriptions.value()),
                                        new Function<Description, String>() {
                                            public String apply(Description description) {
                                                return description.value();
                                            }
                                        }));
                        if (!fullDescription.isEmpty())
                            description = fullDescription;
                    }

                    for (int paramIndex = 0; paramIndex < method.getParameterTypes().length; paramIndex++) {
                        Annotation[] parameterAnnotations = method.getParameterAnnotations()[paramIndex];

                        boolean ignore = false;
                        for (Annotation parameterAnnotation : parameterAnnotations) {
                            if (parameterAnnotation.annotationType().equals(IoDocsIgnore.class))
                                ignore = true;
                        }
                        if (ignore) continue;

                        Class<?> typeClass = method.getParameterTypes()[paramIndex];

                        String paramName = null;
                        String paramDescription = null;
                        boolean required = false;
                        Object defaultValue = null;
                        Location location = null;
                        Type paramType = getType(typeClass);
                        List<String> enumeration = Lists.newArrayList();
                        List<String> enumDescriptions = Lists.newArrayList();

                        //Convert boolean to string "true", "false" enum
                        //because I/O docs treats boolean as 0 or 1
                        if (paramType == Type.BOOLEAN) {
                            enumeration.add("true");
                            enumeration.add("false");
                            enumDescriptions.add("true");
                            enumDescriptions.add("false");
                            paramType = Type.STRING;
                        }

                        boolean isAParameter = false;

                        for (Annotation parameterAnnotation : parameterAnnotations) {
                            if (parameterAnnotation.annotationType().equals(QueryParam.class)) {
                                isAParameter = true;
                                paramName = ((QueryParam) parameterAnnotation).value();
                                location = Location.query;
                            } else if (parameterAnnotation.annotationType().equals(PathParam.class)) {
                                isAParameter = true;
                                paramName = ((PathParam) parameterAnnotation).value();
                                location = Location.pathReplace;
                            } else if (parameterAnnotation.annotationType().equals(HeaderParam.class)) {
                                isAParameter = true;
                                paramName = ((HeaderParam) parameterAnnotation).value();
                                location = Location.header;
                            } else if (parameterAnnotation.annotationType().equals(Description.class)) {
                                paramDescription = ((Description) parameterAnnotation).value();
                            } else if (parameterAnnotation.annotationType().equals(IoDocsDefaultBoolean.class)) {
                                defaultValue = ((IoDocsDefaultBoolean) parameterAnnotation).value() + "";
                            } else if (parameterAnnotation.annotationType().equals(IoDocsDefaultInteger.class)) {
                                defaultValue = ((IoDocsDefaultInteger) parameterAnnotation).value();
                            } else if (parameterAnnotation.annotationType().equals(IoDocsDefaultString.class)) {
                                if (((IoDocsDefaultString) parameterAnnotation).value().contains(":_:")) {
                                    defaultValue = ((IoDocsDefaultString) parameterAnnotation).value().split(":_:")[0];
                                    paramType = Type.TEXTAREA;
                                } else {
                                    defaultValue = ((IoDocsDefaultString) parameterAnnotation).value();
                                }
                            } else if (parameterAnnotation.annotationType().equals(IoDocsRequired.class)) {
                                required = true;
                            } else if (parameterAnnotation.annotationType().equals(IoDocsEnum.class)) {
                                enumeration = Lists.newArrayList(((IoDocsEnum) parameterAnnotation).value());
                            } else if (parameterAnnotation.annotationType().equals(IoDocsEnumDescriptions.class)) {
                                enumDescriptions = Lists.newArrayList(((IoDocsEnumDescriptions) parameterAnnotation).value());
                            }

                        }

                        if (!isAParameter && validRequestBodyClasses.contains(typeClass)) {
                            paramName = "requestBody";
                            paramType = Type.TEXTAREA;
                            location = Location.body;
                        }

                        if (paramName != null)
                            parameters.add(new IoDocsParameter(
                                    paramName, paramDescription, location,
                                    paramType, required, defaultValue,
                                    enumeration, enumDescriptions));
                    }
                }
            }

            if (httpMethod != null)
                methods.add(new IoDocsMethod(
                        name, httpMethod, description, path, parameters));
        }
        return methods;
    }

    private Type getType(Class<?> typeClass) {
        if (Integer.class.isAssignableFrom(typeClass) || int.class.isAssignableFrom(typeClass))
            return Type.INT;
        if (Boolean.class.isAssignableFrom(typeClass) || boolean.class.isAssignableFrom(typeClass))
            return Type.BOOLEAN;
        return Type.STRING;
    }

    public Class<?>[] getClassList(String filePath) {
        // Defining Variables
        Scanner scanner = null;
        ArrayList<String> classList = new ArrayList<String>();

        // Reading Class List File
        try {
            scanner = new Scanner(new File(filePath));
        } catch (Exception ignored) {
        }
        while (scanner.hasNext()) {
            classList.add(scanner.next());
        }
        scanner.close();

        // Saving Class list to a Class Array
        Class<?>[] classArray = new Class<?>[classList.size()];
        for (int counter = 0; counter < classList.size(); counter++) {
            try {
                classArray[counter] = Class.forName(classList.get(counter).trim()).newInstance().getClass();
            } catch (Exception ignored) {
            }
        }

        return classArray;
    }

}
